#region License, Terms and Conditions
//
// Prawks - Copyright (c) 2007 Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

namespace Prawks.CodeProvisioning
{
    using System;
    using System.Reflection;
    using System.Reflection.Emit;

    using ISerializable = System.Runtime.Serialization.ISerializable;
    using SerializationInfo = System.Runtime.Serialization.SerializationInfo;
    using StreamingContext = System.Runtime.Serialization.StreamingContext;
    using Debug = System.Diagnostics.Debug;

    /// <summary>
    /// Compiler for compiling concrete implementations of abstract base classes that
    /// rely on external types to provide implementations.
    /// </summary>
    
    public sealed class TypeImplCompiler
    {
        private TypeImplCompiler()
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Compiles a concrete implementation of an abstract base class.
        /// </summary>
        /// <param name="baseType">
        /// The abstract base type whose concrete implementatin to compile.
        /// </param>
        /// <param name="newTypeName">
        /// The name for the concrete implementation type.
        /// </param>
        /// <param name="targetModule">
        /// The target module where the implementation should be compiled.
        /// </param>
        /// <returns>
        /// A compiled concrete implementation of the given abstract base class.
        /// </returns>
        
        public static Type Compile(Type baseType, string newTypeName, ModuleBuilder targetModule)
        {
            //
            // Validate arguments
            //

            if (baseType == null)
                throw new ArgumentNullException("baseType");

            if (newTypeName == null)
                throw new ArgumentNullException("newTypeName");

            if (newTypeName.Length == 0)
                throw new ArgumentOutOfRangeException("newTypeName", "New type name cannot be an empty string.");

            if (targetModule == null)
                throw new ArgumentNullException("targetModule");

            //
            // Check the incoming type for comptability with the expected 
            // specification. Note that a CompilationException is thrown 
            // rather than an ArgumentOutOfRangeException exception (which 
            // would sound more appropriate because the type's specification 
            // is "out-of-range" of what is expected) in each case of 
            // violation.
            //

            if (baseType.IsInterface)
            {
                throw new CompilationException(
                    string.Format("{0} is an interface. Interface implementation is not currently supported.",
                    baseType.FullName));
            }

            if (!baseType.IsAbstract)
            {
                //
                // This check should catch most cases.
                //

                throw new CompilationException(
                    string.Format("{0} must be an abstract base class.", 
                    baseType.FullName));
            }

            if (baseType.IsValueType)
            {
                throw new CompilationException(
                    string.Format("{0} must be a class. The base type must be a publically inheritable class.",
                    baseType.FullName));
            }

            if (baseType.IsArray)
                throw new CompilationException(baseType.FullName + " is an array and not a valid type for this method. The base type must be a publically inheritable class.");

            if (!baseType.IsPublic && !baseType.IsNestedPublic)
                throw new CompilationException(baseType.FullName + " must be public.");

            if (baseType.IsSealed)
                throw new CompilationException(baseType.FullName + " must be inheritable.");

            //
            // Define a new public class that inherits from the base type.
            // The new type is defined as serializable if the base type is also
            // serializable. This has the same effect as applying the Serializable 
            // pseudo-attribute in most languages that target the runtime. 
            // Support for ISerializable is done a little later on.
            //

            TypeBuilder newType = targetModule.DefineType(newTypeName, 
                TypeAttributes.Public | (baseType.Attributes & TypeAttributes.Serializable), 
                baseType);

            //
            // Implement constructors, copying their definitions from the base
            // type and forwarding each implementation to the base type.
            //

            ImplementConstructors(newType, targetModule.IsTransient());

            //
            // Get all the public method of the base class and compile those
            // that are abstract. All abstract methods of the base type must
            // be bound to an implementation and support compilation.
            //

            int compiledMethodsCount = 0;

            foreach (MethodInfo method in baseType.GetMethods())
            {
                if (!method.IsAbstract)
                    continue;

                IMethodImpl methodImpl = MethodImplBinder.GetImplementation(method);

                IMethodImplCompiler compiler = methodImpl as IMethodImplCompiler;

                if (compiler == null)
                {
                    throw new CompilationException(
                        string.Format("{0} does not support compilation. See {1} in {2}.", 
                        methodImpl.GetType().FullName, method.Name, method.DeclaringType.FullName));
                }

                compiler.Compile(newType);

                compiledMethodsCount++;
            }

            if (compiledMethodsCount == 0)
            {
                throw new CompilationException(
                    string.Format("No methods found to compile in {0}.", baseType.FullName));
            }

            //
            // Add support for ISerializable if the base implements it and it
            // is to become part of a transient module. This adds special 
            // serialization support by overriding the base type's GetObjectData 
            // implementation.
            //
 
            if (baseType.IsSerializable && targetModule.IsTransient())
                ImplementDynamicSerialization(newType);

            return newType.CreateType();
        }

        private static void ImplementDynamicSerialization(TypeBuilder type)
        {
            Debug.Assert(type != null);

            bool hasBaseCustomSerialization = typeof(ISerializable).IsAssignableFrom(type.BaseType);

            //
            // This implementation will first call the base type's GetObjectData and 
            // then add a call to DynamicSerializationSurrogate's GetObjectData method.
            // DynamicSerializationSurrogate is an IObjectReference implementation
            // that supplies itself as the type of the object being serialized. 
            // This has the effect that when the object is deserialized in some
            // other context, DynamicSerializationSurrogate will get a chance to
            // ensure that the type is baked before being used. In other words, 
            // during serialization, we substitue the transient type with a type
            // (DynamicSerializationSurrogate) in a known assembly (this one).
            // Upon deserialization of DynamicSerializationSurrogate, we convert
            // back to the transient type (baking it if necessary).
            //

            MethodInfo serializationMethod = typeof(ISerializable).GetMethod("GetObjectData");

            if (hasBaseCustomSerialization)
            {
                //
                // Start by checking if the base class correctly allows the 
                // ISerializable::GetObjectData method to be publically overridden.
                // If not, then throw an exception.
                //

                InterfaceMapping serializationMappings = type.BaseType.GetInterfaceMap(typeof(ISerializable));
                int serializationMethodIndex = Array.IndexOf(serializationMappings.InterfaceMethods, serializationMethod);
                MethodInfo baseSerializationMethod = serializationMappings.TargetMethods[serializationMethodIndex];
                
                if (!baseSerializationMethod.IsPublic)
                {
                    throw new CompilationException(string.Format("{0} in {1} must be public to enable serialization support.", 
                        baseSerializationMethod.Name, baseSerializationMethod.DeclaringType.FullName));
                }

                if (baseSerializationMethod.IsFinal)
                {
                    throw new CompilationException(string.Format("{0} in {1} must be overridable to enable serialization support.",
                        baseSerializationMethod.Name, baseSerializationMethod.DeclaringType.FullName));
                }
            }
            else
            {
                type.AddInterfaceImplementation(typeof(ISerializable));
            }

            //
            // Define a new serialization method (GetObjectData) by overriding the
            // base type's implementation.
            //

            MethodBuilder newSerializationMethod = type.DefineMethod("GetObjectData", 
                MethodAttributes.Public | MethodAttributes.Virtual,
                CallingConventions.Standard, null, 
                new Type[] { typeof(SerializationInfo), typeof(StreamingContext) });

            newSerializationMethod.DefineParameter(1, ParameterAttributes.None, "info");
            newSerializationMethod.DefineParameter(2, ParameterAttributes.None, "context");

            ILGenerator il = newSerializationMethod.GetILGenerator();

            //
            // Start out by calling the base type's implementation if it provided one. 
            // This will capture its serialization data.
            //

            if (hasBaseCustomSerialization)
            {
                il.Emit(OpCodes.Ldarg_0); // Push this reference
                il.Emit(OpCodes.Ldarg_1); // Push info
                il.Emit(OpCodes.Ldarg_2); // Push context
                il.EmitCall(OpCodes.Call, type.BaseType.GetMethod("GetObjectData"), null);
            }

            //
            // Next, place a call to DynamicSerializationSurrogate::GetObjectData
            // to morph the transient type into a persistent type.
            //

            Type carrierType = hasBaseCustomSerialization ?
                typeof(DynamicCustomSerializationSurrogate) : 
                typeof(DynamicMemberSerializationSurrogate);
            
            il.Emit(OpCodes.Ldarg_0); // Push this reference
            il.Emit(OpCodes.Newobj, carrierType.GetConstructor(new Type[] { typeof(object) }));

            il.Emit(OpCodes.Ldarg_1); // Push info
            il.Emit(OpCodes.Ldarg_2); // Push context
            il.EmitCall(OpCodes.Call, carrierType.GetMethod("GetObjectData"), null);
            
            il.Emit(OpCodes.Ret);

            if (!hasBaseCustomSerialization)
                type.DefineMethodOverride(newSerializationMethod, serializationMethod);
        }

        private static void ImplementConstructors(TypeBuilder type, bool isTransient)
        {
            //
            // Get all public and non-public constructors on the base type.
            //

            ConstructorInfo[] baseConstructors = type.BaseType.GetConstructors(
                BindingFlags.Instance | BindingFlags.Public | 
                BindingFlags.NonPublic | BindingFlags.DeclaredOnly);

            if (baseConstructors.Length == 0)
            {
                throw new CompilationException(
                    string.Format("{0} has no accessible constructors.",
                    type.BaseType.FullName));
            }

            //
            // Walk through all the constructors (skipping private ones in
            // case we get them) and implement each of them.
            //

            foreach (ConstructorInfo baseConstructor in baseConstructors)
            {
                if (baseConstructor.IsPrivate)
                    continue;

                //
                // Define the new constructor identical to the one from the 
                // base class. Member access to constructors is forced to
                // public for a transient type so that they can be instantiated.
                // For persistent type, the member access is left the same as
                // the base type desired. However, the deserialization
                // constructor is left with protected access always. A 
                // deserialization constructor is recognized by examining its
                // parameter list for the following sequence:
                //
                //  .ctor(SerializationInfo, StreamingContext)
                //

                ParameterInfo[] parameters = baseConstructor.GetParameters();
                Type[] paramTypes = TypeCollection.FromParameters(parameters);

                bool isDeserializer = paramTypes.Length == 2 &&
                                      paramTypes[0] == typeof(SerializationInfo) &&
                                      paramTypes[1] == typeof(StreamingContext);

                MethodAttributes attributes = baseConstructor.Attributes;

                if (isTransient)
                {
                    attributes = (attributes & ~MethodAttributes.MemberAccessMask) |
                        (isDeserializer ? MethodAttributes.Family : MethodAttributes.Public);
                }

                ConstructorBuilder newConstructor = type.DefineConstructor(
                    attributes, CallingConventions.Standard, paramTypes);

                ILGenerator il = newConstructor.GetILGenerator();

                //
                // Emit IL to simply call the base constructor. Note that 
                // we push parameters and emit additional metadata on them in 
                // the same loop to avoid looping twice.
                //

                il.Emit(OpCodes.Ldarg_0); // Push this reference

                for (int i = 0; i < parameters.Length; i++)
                {
                    newConstructor.DefineParameter(i + 1, parameters[i].Attributes, parameters[i].Name);
                    il.Emit(OpCodes.Ldarg, i + 1);
                }

                il.Emit(OpCodes.Call, baseConstructor);
                il.Emit(OpCodes.Ret);
            }
        }
    }
}
